###################################################################### Product:      KnowledgeEngine (Plone Edition)## File:         ScoreSummaryReport.py## Copyright:    Copyright (c) 2005, Faulkner Technologies## Author:       Brian Oliver## Description:  This report generates a graph of score bands#               based on a scored report.  Additionally calculates
#               average, minimum and maximum scores.
#
#               This report may only be generated with a locked model.
## See Also:     GenericObject, Component, Model, Section#####################################################################

####################################################################### Knowledge Engine Library Importsfrom Products.KnowledgeEngine.Common import *from Products.KnowledgeEngine.Core import *from Products.KnowledgeEngine.AssessmentFilter import AssessmentFilter
from Products.KnowledgeEngine.MultipleAssessmentReport import MultipleAssessmentReport, MultipleAssessmentReportInfofrom Products.KnowledgeEngine.PropertyDefinition import *from Products.KnowledgeEngine.PropertyTypes import *from Products.KnowledgeEngine.PropertyFormats import *from Products.KnowledgeEngine.PropertyStructures import *from Products.KnowledgeEngine.EventDefinition import *
from Products.KnowledgeEngine.WizardStepDefinition import *
from Products.KnowledgeEngine.Graphing.Graphable import *
from Products.KnowledgeEngine.Graphing.Datum import *
from Products.KnowledgeEngine.KERLExtensions import KERLExtensions
from Products.KnowledgeEngine.KERLProcessor import *

####################################################################### Zope Library Importsfrom Globals import DTMLFilefrom ImageFile import ImageFile

####################################################################### Python Library Importsimport sys
import time

#####################################################################
## Python Logging Configuration
import logging
log = logging.getLogger("KnowledgeEngine.Reporting")

####################################################################### ScoreSummaryReportInfo Class Definitionclass ScoreSummaryReportInfo(MultipleAssessmentReportInfo):    """        Defines the class info methods, defining information about        the class without needing an instance of the class    """        #---------------------------------------------------------------#        def getClassName(self):        """            Returns the name of the class for which             we are providing information.        """        return 'ScoreSummaryReport'
        
    #---------------------------------------------------------------#
    
    def getDisplayName(self):        """            Return a friendly name for the class instances            (this is used for displaying in lists etc)        """            return "Score Summary Report"            #---------------------------------------------------------------#
    
    def getIdentityType(self):        """            Return the identity type.          """                return "ScoreSummaryReport"            #---------------------------------------------------------------#        def getClassIcon(self):        """            Return the appropriate icon file for the class.        """                return ImageFile('skins/ScoreSummaryReport_icon.gif', globals())        #---------------------------------------------------------------#
    
    def onConfigureProperties(self):        """            Automatically called by the Knowledge Engine once            the class has been loaded.  Use this method to register             class specific properties using the PropertyReflection interface.                        If this class is sub-classed, a call must be made to            the super-class to ensure super-class properties are            correctly registered                        NOTE: This method is called before ClassInfo.onAfterStartup()                    """
        
        #initialise the super-class properties for reflection        MultipleAssessmentReportInfo.onConfigureProperties(self)        #---------------------------------------------------------------#
    
    def onConfigureEvents(self):        """            Automatically called by the Knowledge Engine once            the class has been loaded.  Use this method to register             class specific events using the EventReflection interface.                        If this class is sub-classed, a call must be made to            the super-class to ensure super-class properties are            correctly registered                        NOTE: This method is called before ClassInfo.onAfterStartup()                    """                        #initialise the super-class events for reflection        MultipleAssessmentReportInfo.onConfigureEvents(self)
        
    #---------------------------------------------------------------#
    
####################################################################### ScoreSummaryReport Class Definitionclass ScoreSummaryReport(MultipleAssessmentReport):    """        A class to represent a report consisting of graphs of        component state
    """
    
    #---------------------------------------------------------------#
    
    #ABSTRACT TRANSIENT: a ClassInfo implementation instance to provide info about this class    _CLASSINFO = ScoreSummaryReportInfo()
    
    #---------------------------------------------------------------#
    
    def __init__(self, id, identity):        "Constructor for the object"        
        MultipleAssessmentReport.__init__(self, id, identity)        
    #---------------------------------------------------------------#

    def isLockedModelRequired(self):
        """
            ABSTRACT: Returns if the report must have a locked model
            in order to be generated.
        """
        
        #models must be locked to generate this report
        return true
        
    #---------------------------------------------------------------#
        
    def getWizardStepDefinitions(self, context=None):
        """
            ABSTRACT: This method is used by this class to
            determine the actual wizard step definitions
            for the wizard
            
            RETURN: [WizardStepDefinition]
        """
        
        #get all of the available ScoredReports that we can use for calculating scores
        scoredReports = self.getModel().getReports(user = None, className = "ScoredReport")

        #let the user choose the scoring mechanism                     
        wizardStep1Definition = WizardStepDefinition("Score Calculation and Presentation", "Choose how assessment scores will be calculated & categorized.")
        wizardStep1Definition.addPropertyDefinition(PropertyDefinition(identity="scoredReportIdentity", displayName="Available Scoring Mechanisms", description="Please select how the assessment scores will be calculated.", defaultValue="", propertyType=pt_STRING, propertyFormat=pf_NATIVE, propertyStructure=ps_ATOMIC, isForConstructor=false, hasEnumeratedValues=true, enumeratedValuesMapping=[(report.getTitleOrId(), report.getIdentity()) for report in scoredReports]))

        #the number of scoring bands
        wizardStep1Definition.addPropertyDefinition(PropertyDefinition(identity="nrBands", displayName="Number of Score Distribution Bands", description="The number of bands for scores.", defaultValue=5, propertyType=pt_INTEGER, propertyFormat=pf_NATIVE, propertyStructure=ps_ATOMIC, isForConstructor=false, hasEnumeratedValues=true, enumeratedValuesList=range(1,11)))

        return [wizardStep1Definition]

    #---------------------------------------------------------------#

    def renderReport(self, request, wizardState, **args):
        """
            Renders the report content (called by Report.generate)
            
            NOTE: Override this method to perform specific
            report generation/rendering using the provided
            parameters
        """

        starttime = time.clock()
        log.debug("%s:(%d): %s" % (sys._getframe().f_code.co_name, sys._getframe().f_lineno, "[commenced]: Generating Score Histogram Report"))        

        #get the assessment storage service... we'll need that to manage the assessment
        assessmentStorageService = getAssessmentStorageService(self)

        #get the current user that is rendering the report
        user = request["USER"]
    
        #get the session from the request
        session = request["SESSION"]

        #get the response from the request (weird I know, but that's where it lives in Zope)
        response = request["RESPONSE"]

        #get the scored report used for calculating scores
        scoredReportIdentity = wizardState.getPropertyValue("scoredReportIdentity")
        scoredReport = self.getModel().getComponent(scoredReportIdentity)

        #get the assessment filter to use to filter out assessments from the session 
        #(this is typically set by the assessment explorer)
        assessmentFilter = wizardState.getPropertyValue("assessmentFilter")
        
        #determine the number of assessments that satisfy the filter
        nrAssessments = assessmentStorageService.getNrAssessmentsSatisfyingFilter(assessmentFilter, user)   
        wizardState.setPropertyValue("nrAssessments", nrAssessments)
        request.set('NRASSESSMENTS', nrAssessments)
        
        #calculate the min, max, average and scores distribution
        minimumScore = None
        maximumScore = None
        passCount = 0
        totalScore = 0.0

        #get all of the scores
        scores = []       
        for assessment in assessmentStorageService.getAssessmentsSatisfyingFilter(assessmentFilter, user):
            #get the score for the assessment
            score = scoredReport.getScore(assessment)

            #add the score to the total
            totalScore = totalScore + score
            
            #is it the minimum?
            if minimumScore is None or score < minimumScore:
                minimumScore = score
                
            #is it the maximum?
            if maximumScore is None or score > maximumScore:
                maximumScore = score

            #did the assessment pass?
            if scoredReport.hasPassScore() and score >= scoredReport.getPassScore():
                passCount = passCount + 1

            #remember the score
            scores = scores + [score]

        #determine an appropriate and sensible distribution band size
        nrBands = wizardState.getPropertyValue("nrBands")
        nrDecimalPlaces = 2
        bandSize = round((maximumScore + 1) / nrBands, nrDecimalPlaces)
        while bandSize == 0.0:
            nrDecimalPlaces = nrDecimalPlaces + 1
            bandSize = round((maximumScore + 1) / nrBands, nrDecimalPlaces)
        
        #create default distribution of scores using the selected band size
        distribution = {}
        for band in range(0, nrBands):
            distribution[band] = 0
        
        #add the scores to the distribution
        for score in scores:
            #determine band
            band = int(score // bandSize)
            
            #add to the band
            distribution[band] = distribution[band] + 1
        
        #generate a graph of the score distribution
        distributionGraphable = Graphable("Score Distribution", "The following chart details assessment score distribution for the '%s' scored report." % (scoredReport.getTitleOrId(),), 0, nrAssessments)
        for band in distribution.keys():
            filterTitle = "Assessment In Range"
            filterExpression = parseKERL("['%s', 'scoreinrange', %f, %f]" % (scoredReport.getIdentity(), band * bandSize,(band + 1) * bandSize))
            datumAssessmentFilter = AssessmentFilter(title=filterTitle, expression=simplifyKERL(['all', assessmentFilter.getExpression(), filterExpression]), model=self.getModel(), assessmentScope=assessmentFilter.getAssessmentScope())
            datum = Datum(identity="%d" % band, title="%.*f ... %.*f:" % (nrDecimalPlaces, band * bandSize, nrDecimalPlaces, (band + 1) * bandSize), value=distribution[band], color="green", context={"ASSESSMENT_FILTER":datumAssessmentFilter})
            distributionGraphable.addDatum(datum)

        #calculate the average score
        if nrAssessments > 0:
            averageScore = totalScore / nrAssessments
        else:
            averageScore = 0.0

        #generate a graph of the pass/fail distribution
        if scoredReport.hasPassScore():
            passFailGraphable = Graphable("Pass/Fail Distribution", "The following chart details pass/fail distribution of assessments for the '%s' scored report." % (scoredReport.getTitleOrId(),), 0, nrAssessments)

            #create a datum for the passing assessments
            filterTitle = "Passing Assessments"
            filterExpression = parseKERL("['%s', 'pass']" % scoredReport.getIdentity())
            datumAssessmentFilter = AssessmentFilter(title=filterTitle, expression=simplifyKERL(['all', assessmentFilter.getExpression(), filterExpression]), model=self.getModel(), assessmentScope=assessmentFilter.getAssessmentScope())
            datum = Datum(identity="passed", title="Passed:", value=passCount, color="green", context={"ASSESSMENT_FILTER":datumAssessmentFilter})
            passFailGraphable.addDatum(datum)

            #create a datum for the failing assessments
            filterTitle = "Failing Assessments"
            filterExpression = parseKERL("['%s', 'fail']" % scoredReport.getIdentity())
            datumAssessmentFilter = AssessmentFilter(title=filterTitle, expression=simplifyKERL(['all', assessmentFilter.getExpression(), filterExpression]), model=self.getModel(), assessmentScope=assessmentFilter.getAssessmentScope())
            datum = Datum(identity="failed", title="Failed:", value=nrAssessments-passCount, color="red", context={"ASSESSMENT_FILTER":datumAssessmentFilter})
            passFailGraphable.addDatum(datum)
        else:
            passFailGraphable = None

        #add the generated stats and graphs to the wizard state (for the report to use)
        wizardState.setPropertyValue("distributionGraphable", distributionGraphable)
        wizardState.setPropertyValue("passFailGraphable", passFailGraphable)
        wizardState.setPropertyValue("minimumScore", minimumScore)
        wizardState.setPropertyValue("maximumScore", maximumScore)
        wizardState.setPropertyValue("averageScore", averageScore)
        wizardState.setPropertyValue("passScore", scoredReport.getPassScore())
        
        #generate the report
        report = MultipleAssessmentReport.renderReport(self, request, wizardState, **args)

        endtime = time.clock()
        log.debug("%s:(%d): %s" % (sys._getframe().f_code.co_name, sys._getframe().f_lineno, "Report Generation Time: %f" % (endtime-starttime)))        
        log.debug("%s:(%d): %s" % (sys._getframe().f_code.co_name, sys._getframe().f_lineno, "[completed]: Generating Score Histogram Report"))        
        
        return report
        
    #---------------------------------------------------------------#

####################################################################### Class Initialisation
# Register Class Information with the Knowledge EngineregisterGenericObjectClass(ScoreSummaryReport)
#####################################################################
